﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.VersionControl.Client;
using Microsoft.TeamFoundation.WorkItemTracking.Client;
using System.Security.Cryptography;
using System.IO;

namespace TFSTibcoDesignerRCSAdapter
{
    internal class TfsOperations: IDisposable
    {
        private readonly TfsTeamProjectCollection _tfsTeamProjectCollection;
        private readonly VersionControlServer _versionControlServer;
        private readonly Workspace _workspace;

        private const string _passPhrase = "P@55pr@se";        // can be any string
        private const string _saltValue = "s@1tValue";        // can be any string
        private const string _hashAlgorithm = "SHA1";             // can be "MD5"
        private const int _passwordIterations = 2;                  // can be any number
        private const string _initVector = "@1B2c3D4e5F6g7H8"; // must be 16 bytes
        private const int _keySize = 256; 

        /// <summary>
        /// Implementation of Dispose method because it implements Disposable
        /// </summary>
        public void Dispose()
        {
            _tfsTeamProjectCollection.Dispose();
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// NoArg constructor, Initiates the TFS connectivity
        /// </summary>
        internal TfsOperations()
        {
            try
            {
                if (!SystemConfig.Default.TeamFSUsername.Equals("") &&
                    !SystemConfig.Default.TeamFSPassword.Equals(""))
                {
                    System.Net.NetworkCredential networkCredentials;
                    if (!SystemConfig.Default.TeamFSDomain.Equals(""))
                    {
                        networkCredentials = new System.Net.NetworkCredential(SystemConfig.Default.TeamFSUsername, Decrypt(SystemConfig.Default.TeamFSPassword));
                    }
                    else
                    {
                        networkCredentials = new System.Net.NetworkCredential(SystemConfig.Default.TeamFSUsername, Decrypt(SystemConfig.Default.TeamFSPassword), SystemConfig.Default.TeamFSDomain);
                    }
                    _tfsTeamProjectCollection =
                        new TfsTeamProjectCollection(new Uri(SystemConfig.Default.TeamFSURL), networkCredentials);
                }
                else
                {
                    _tfsTeamProjectCollection =
                        new TfsTeamProjectCollection(new Uri(SystemConfig.Default.TeamFSURL));
                }
                if (_tfsTeamProjectCollection != null)
                {
                    _tfsTeamProjectCollection.EnsureAuthenticated();

                    // Connect to TFS Source Control
                    _versionControlServer =
                        (VersionControlServer)_tfsTeamProjectCollection.GetService(typeof(VersionControlServer));
                }
                else
                {
                    throw new Exception("Cannot Open TFS connection"); 
                }
            }
            catch (Exception exc)
            {
                LoggingMethods.Logerr(exc.Message + ":" + ((exc.InnerException == null) ? "" : exc.InnerException.ToString()));
            }
        }

        /// <summary>
        /// Constructor with argument the filepath to which it will instantiate the workspace object
        /// </summary>
        /// <param name="sfilepath"></param>
        internal TfsOperations(string sfilepath):this()
        {
            _workspace = _versionControlServer.TryGetWorkspace(sfilepath);
            if (_workspace==null)
            {
                LoggingMethods.Logerr(String.Format("Error:Cannot Instantiate Workspace for path {0}",sfilepath));
                throw new Exception("Cannot Open workspace in TFS"); 
            }
            
        }
        
        /// <summary>
        /// Actual implementation to add files in TFS. Folders and substitution
        /// variables are treated similarly, because groups in substitution
        /// variables are just folders with additional '.substvar' fil
        /// </summary>
        /// <param name="sfilepath"></param>
        /// <returns></returns>
        internal bool AddItem(string sfilepath)
        {
            var retvar = false;
            if (sfilepath.Length == 0)
            {
                LoggingMethods.Debug("Error: In Add command the input filepath was empty!");
                return false;
            }
            try
            {
                //Add Pending file non recursively into Workspace
                int itemsadded = _workspace.PendAdd(sfilepath, false); 
                if (itemsadded <= 0)
                {
                    LoggingMethods.Debug("Error: Not any item added");
                    retvar = false;
                }
                else
                    retvar = true;
            }
            catch (Exception ex)
            {
                LoggingMethods.Logerr(ex.InnerException.ToString());
                retvar = false;
            }
            return retvar;
        }

        /// <summary>
        /// This method is probably the most important and most frequently called.
        /// The results are cached by the ScriptableAdapter host. Essentially this
        /// tells TIBCO Designer whether the file is under revision control or not.
        /// Caution: TIBCO Designer WILL ask about child files that are in a container that
        /// is not revision controlled! Other methods call this to determine wether
        /// they should do any work or not.
        /// </summary>
        /// <param name="sfilepath"></param>
        /// <returns></returns>
        internal bool IsRevisionControlled(string sfilepath)
        {
            bool returnvalue = true;

            if (String.IsNullOrEmpty(sfilepath))
                return false;
            try
            {
                string serverItemPath = _workspace.TryGetServerItemForLocalItem(sfilepath);
                var testItemExists = _versionControlServer.ServerItemExists(serverItemPath, ItemType.Any);

                returnvalue = testItemExists;
            }
            catch (Exception ex)
            {
                LoggingMethods.Logerr(ex.Message + ":" + (ex.InnerException.ToString()??""));
            }
            return returnvalue;
        }


        /// <summary>
        /// Get Pending Changes and returns list of them from specific filepath
        /// </summary>
        /// <param name="sfilepath"></param>
        /// <returns></returns>
        internal List<PendingChange> GetPendingChanges(string sfilepath)
        {
            List<PendingChange> retval = new List<PendingChange>(0);
            try
            {
                string serverItemPath = _workspace.TryGetServerItemForLocalItem(sfilepath);
                
                ItemSpec[] itemSpecs = new ItemSpec[1];
                itemSpecs[0] = new ItemSpec(serverItemPath,
                                            RecursionType.Full);
                PendingSet[] pendingSet = _versionControlServer.QueryPendingSets(itemSpecs, "", "");

                if (pendingSet.Length > 0)
                {
                    if (pendingSet[0].PendingChanges.Length > 0)
                    {
                        retval =  pendingSet[0].PendingChanges.ToList();
                    }
                }
            }
            catch (Exception ex)
            {
                LoggingMethods.Logerr(ex.Message + ":" + ((ex.InnerException==null)?"": ex.InnerException.ToString()));
            }
            return retval;
        }
        
        internal string GetPendingChangesStringFormat(string sfilepath)
        {
            var pendingChanges = GetPendingChanges(sfilepath);
            string retval = "No changes exist to be performed";
            if (pendingChanges.Count()>0)
                 retval = pendingChanges.Aggregate("", (current, pendingchange) => current + (pendingchange.ChangeTypeName + " file " + pendingchange.FileName + " from " + pendingchange.LocalOrServerFolder + "\r\n"));
            
            return retval;
        }


        internal string EvaluateCheckIn(List<PendingChange> pendingChanges)
        {
            string retval = "";
            try
            {
                if (pendingChanges.Count() > 0)
                {
                    var checkInEvaluation = _workspace.EvaluateCheckin(CheckinEvaluationOptions.Conflicts, pendingChanges.ToArray(), pendingChanges.ToArray(), "", null, null);

                    if (checkInEvaluation.Conflicts.Count() > 0)
                    {
                        retval = "There are conflicts in the project. Please resolve them before go on:";

                        retval = checkInEvaluation.Conflicts.Aggregate("", (current, conflict) => current + (conflict.Message+ "\r\n"));
                    }
                    return retval;
                }
                else
                {
                    return String.Empty;
                }

            }
            catch (Exception ex)
            {
                LoggingMethods.Logerr(ex.Message + ":" + ((ex.InnerException == null) ? "" : ex.InnerException.ToString()));
                return retval;
            }

        }
        /// <summary>
        /// If the user provided a file, then TIBCO Designer had a selection. Only
        /// commit that file. Otherwise the entire project. Because SVN keeps
        /// track locally, we auto-commit deletions to try and clean up directories
        /// that are being deleted, in these cases we pass a specific file.
        /// </summary>
        /// <param name="sfilepath"></param>
        /// <param name="scomment"></param>
        internal bool CheckInPendingChanges(string sfilepath, string scomment)
        {
            int taskId = 0;
            int ChangesetNum = 0;
            try
            {
                List<PendingChange> pendingChanges = GetPendingChanges(sfilepath);
                if (pendingChanges.Count()>0)
                {
                    if (scomment.StartsWith("##"))
                    {
                        var strtaskId = scomment.Substring(2, scomment.IndexOf("##", 2)-2);
                        if (!Int32.TryParse(strtaskId, out taskId))
                            taskId = 0;
                    }
                    if (taskId==0)
                    {
                        ChangesetNum = _workspace.CheckIn(pendingChanges.ToArray(), scomment);
                    }
                    else
                    {
                        CheckinNote checkinNote = null;
                        
                        WorkItemCheckinInfo[] workItemCheckinInfo = new WorkItemCheckinInfo[1];
                        PolicyOverrideInfo policyOverrideInfo = new PolicyOverrideInfo(scomment, new PolicyFailure[0]);
                        WorkItem workItem = (new WorkItemStore(_tfsTeamProjectCollection)).GetWorkItem(taskId);

                        workItemCheckinInfo[0] = new WorkItemCheckinInfo(workItem, WorkItemCheckinAction.Associate);
                        ChangesetNum = _workspace.CheckIn(pendingChanges.ToArray(),
                                                    scomment,
                                                    checkinNote,
                                                    workItemCheckinInfo,
                                                    policyOverrideInfo
                                                    );
                    }


                    if (ChangesetNum > 0)
                    {
                        LoggingMethods.Debug(String.Format("Check In successful: Changeset number:{0}", ChangesetNum));
                        return true;
                    }
                    else if (ChangesetNum == 0)
                    {
                        Console.Out.WriteLine("No changes to be checked in!");
                        return false;
                    }
                    else
                        return false;

                }
                else
                {
                    return false;
                }
                
            }
            catch (Exception ex)
            {
                LoggingMethods.Logerr(ex.Message + ":" + ((ex.InnerException==null)?"": ex.InnerException.ToString()));
                return false;
            }
            
        }

        /// <summary>
        /// Marks file - folder as Pending for Edit. Do this also recursively.
        /// </summary>
        /// <param name="sfilepath"></param>
        /// <param name="isRecursive"></param>
        /// <returns></returns>
        internal bool CheckOutItem(string sfilepath, bool isRecursive)
        {
            if (String.IsNullOrEmpty(sfilepath))
            {
                LoggingMethods.Debug("Error: Cannot Edit (checkout) an empty item.");
                return false;
            }
            try
            {
                string serverItemPath = _workspace.TryGetServerItemForLocalItem(sfilepath);

                if (String.IsNullOrEmpty(serverItemPath))
                {
                    LoggingMethods.Debug(String.Format("Error: Cannot find Server Item Path:{0}", serverItemPath));
                    return false;
                }
                //var response = _workspace.PendEdit(sfilepath, IsRecursive(sfilepath) ? RecursionType.Full : RecursionType.None);
                var response = _workspace.PendEdit(sfilepath, isRecursive ? RecursionType.Full : RecursionType.None);
                if (response > 0)
                    return true;
                else
                    return false;
            }
            catch (Exception ex)
            {
                LoggingMethods.Logerr(ex.Message + ":" + ((ex.InnerException==null)?"": ex.InnerException.ToString()));
                return false;
            }
        }
        
        /// <summary>
        /// Performs and undo of Pending changes = reverting back
        /// </summary>
        /// <param name="sfilepath"></param>
        /// <param name="isRecursive"></param>
        /// <returns></returns>
        internal bool UndoPendingChanges(string sfilepath, bool isRecursive)
        {
            if (String.IsNullOrEmpty(sfilepath))
            {
                LoggingMethods.Debug("Error: Cannot Edit (checkout) an empty item.");
                return false;
            }
            try
            {
                string serverItemPath = _workspace.TryGetServerItemForLocalItem(sfilepath);

                if (String.IsNullOrEmpty(serverItemPath))
                {
                    LoggingMethods.Debug(String.Format("Error: Cannot find Server Item Path:{0}", serverItemPath));
                    return false;
                }

                var response = _workspace.Undo(sfilepath, isRecursive ? RecursionType.Full : RecursionType.None);
                if (response >= 0)
                    return true;
                else
                    return false;
            }
            catch (Exception ex)
            {
                LoggingMethods.Logerr(ex.Message + ":" + ((ex.InnerException==null)?"": ex.InnerException.ToString()));
                return false;
            }
        }

        /// <summary>
        ///  Marks files, folders for deletion recursively
        /// </summary>
        /// <param name="sfilepath"></param>
        /// <returns></returns>
        internal bool DeleteItem(string sfilepath)
        {
            try
            {
                string serverItemPath = _workspace.TryGetServerItemForLocalItem(sfilepath);

                if (String.IsNullOrEmpty(serverItemPath))
                {
                    LoggingMethods.Debug(String.Format("Error: Cannot find Server Item Path:{0}", serverItemPath));
                    return false;
                }
                var forDeletionfiles = _workspace.PendDelete(sfilepath, RecursionType.Full);
                if (forDeletionfiles>=0)
                    return true;
                else
                    return false;
            }
            catch (Exception ex)
            {
                LoggingMethods.Logerr(ex.Message + ":" + ((ex.InnerException==null)?"": ex.InnerException.ToString()));
                return false;
            }
        }

        /// <summary>
        /// Performs Get Latest  with Overwrite method on a specific file path recursively
        /// </summary>
        /// <param name="sfilepath"></param>
        /// <returns></returns>
        internal bool GetLatestVersion(string sfilepath)
        {
            try
            {
                string serverItemPath = _workspace.TryGetServerItemForLocalItem(sfilepath);
                
                var itemSpec = new ItemSpec(serverItemPath,
                                            RecursionType.Full);

                var getRequest = new GetRequest(itemSpec, VersionSpec.Latest);
                GetStatus getStatus = _workspace.Get(getRequest,GetOptions.Overwrite);

                var response = "Get Latest version successful:" + "\r\n";
                response += "Action Performed:" + (getStatus.NoActionNeeded ? "false" : "true") + "\r\n";
                response += "Num Of Conflicts:" + getStatus.NumConflicts.ToString() + "\r\n";
                response += "Num Of Failures:" + getStatus.NumFailures.ToString() + "\r\n";
                response += "Num Of Operations:" + getStatus.NumOperations.ToString() + "\r\n";
                response += "Num Of Updates:" + getStatus.NumUpdated.ToString() + "\r\n";

                if (getStatus.NumConflicts > 0 || getStatus.NumFailures > 0)
                {
                    LoggingMethods.Logerr(response);
                    return false;
                }
                else
                {
                    LoggingMethods.Debug(response);
                    return true;
                }

            }
            catch (Exception ex)
            {
                LoggingMethods.Logerr(ex.Message + ":" + ((ex.InnerException == null) ? "" : ex.InnerException.ToString()));
                return false;
            }

        }

        /// <summary>
        /// obsolete
        /// </summary>
        /// <param name="sfilepath"></param>
        /// <returns></returns>
        private static bool IsRecursive(string sfilepath)
        {
            if ((sfilepath.LastIndexOf('/')>0 && sfilepath.Substring(sfilepath.LastIndexOf('/')).IndexOf('.') > 0) || 
                (sfilepath.LastIndexOf('\\')>0 &&sfilepath.Substring(sfilepath.LastIndexOf('\\')).IndexOf('.') > 0))
                return false;
            else
                return true;


        }

        /// <summary>
        /// Tries to encrypt given plainText with rindjael algorithm
        /// </summary>
        /// <param name="plainText"></param>
        /// <returns></returns>
        internal string Encrypt(string plainText)
        {
            try
            {
                // Convert strings into byte arrays.
                // Let us assume that strings only contain ASCII codes.
                // If strings include Unicode characters, use Unicode, UTF7, or UTF8 
                // encoding.
                byte[] initVectorBytes = Encoding.ASCII.GetBytes(_initVector);
                byte[] saltValueBytes = Encoding.ASCII.GetBytes(_saltValue);

                // Convert our plaintext into a byte array.
                // Let us assume that plaintext contains UTF8-encoded characters.
                byte[] plainTextBytes = Encoding.UTF8.GetBytes(plainText);

                // First, we must create a password, from which the key will be derived.
                // This password will be generated from the specified passphrase and 
                // salt value. The password will be created using the specified hash 
                // algorithm. Password creation can be done in several iterations.
                PasswordDeriveBytes password = new PasswordDeriveBytes(
                                                                _passPhrase,
                                                                saltValueBytes,
                                                                _hashAlgorithm,
                                                                _passwordIterations);

                // Use the password to generate pseudo-random bytes for the encryption
                // key. Specify the size of the key in bytes (instead of bits).
                byte[] keyBytes = password.GetBytes(_keySize / 8);

                // Create uninitialized Rijndael encryption object.
                RijndaelManaged symmetricKey = new RijndaelManaged();

                // It is reasonable to set encryption mode to Cipher Block Chaining
                // (CBC). Use default options for other symmetric key parameters.
                symmetricKey.Mode = CipherMode.CBC;

                // Generate encryptor from the existing key bytes and initialization 
                // vector. Key size will be defined based on the number of the key 
                // bytes.
                ICryptoTransform encryptor = symmetricKey.CreateEncryptor(
                                                                 keyBytes,
                                                                 initVectorBytes);

                // Define memory stream which will be used to hold encrypted data.
                MemoryStream memoryStream = new MemoryStream();

                // Define cryptographic stream (always use Write mode for encryption).
                CryptoStream cryptoStream = new CryptoStream(memoryStream,
                                                             encryptor,
                                                             CryptoStreamMode.Write);
                // Start encrypting.
                cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length);

                // Finish encrypting.
                cryptoStream.FlushFinalBlock();

                // Convert our encrypted data from a memory stream into a byte array.
                byte[] cipherTextBytes = memoryStream.ToArray();

                // Close both streams.
                memoryStream.Close();
                cryptoStream.Close();

                // Convert encrypted data into a base64-encoded string.
                string cipherText = Convert.ToBase64String(cipherTextBytes);

                // Return encrypted string.
                return cipherText;
            }
            catch (Exception exc)
            {
                var error = "Error Occured in Encrypting string:" + exc.Message;
                LoggingMethods.Logerr(error);
                return error;
            }
        }

        /// <summary>
        /// Tries to Decrypt given plainText with rindjael algorithm
        /// </summary>
        /// <param name="cipherText"></param>
        /// <returns></returns>
        internal string Decrypt(string cipherText)
        {
            try
            {
                // Convert strings defining encryption key characteristics into byte
                // arrays. Let us assume that strings only contain ASCII codes.
                // If strings include Unicode characters, use Unicode, UTF7, or UTF8
                // encoding.
                byte[] initVectorBytes = Encoding.ASCII.GetBytes(_initVector);
                byte[] saltValueBytes = Encoding.ASCII.GetBytes(_saltValue);

                // Convert our ciphertext into a byte array.
                byte[] cipherTextBytes = Convert.FromBase64String(cipherText);

                // First, we must create a password, from which the key will be 
                // derived. This password will be generated from the specified 
                // passphrase and salt value. The password will be created using
                // the specified hash algorithm. Password creation can be done in
                // several iterations.
                PasswordDeriveBytes password = new PasswordDeriveBytes(
                                                                _passPhrase,
                                                                saltValueBytes,
                                                                _hashAlgorithm,
                                                                _passwordIterations);

                // Use the password to generate pseudo-random bytes for the encryption
                // key. Specify the size of the key in bytes (instead of bits).
                byte[] keyBytes = password.GetBytes(_keySize / 8);

                // Create uninitialized Rijndael encryption object.
                RijndaelManaged symmetricKey = new RijndaelManaged();

                // It is reasonable to set encryption mode to Cipher Block Chaining
                // (CBC). Use default options for other symmetric key parameters.
                symmetricKey.Mode = CipherMode.CBC;

                // Generate decryptor from the existing key bytes and initialization 
                // vector. Key size will be defined based on the number of the key 
                // bytes.
                ICryptoTransform decryptor = symmetricKey.CreateDecryptor(
                                                                 keyBytes,
                                                                 initVectorBytes);

                // Define memory stream which will be used to hold encrypted data.
                MemoryStream memoryStream = new MemoryStream(cipherTextBytes);

                // Define cryptographic stream (always use Read mode for encryption).
                CryptoStream cryptoStream = new CryptoStream(memoryStream,
                                                              decryptor,
                                                              CryptoStreamMode.Read);

                // Since at this point we don't know what the size of decrypted data
                // will be, allocate the buffer long enough to hold ciphertext;
                // plaintext is never longer than ciphertext.
                byte[] plainTextBytes = new byte[cipherTextBytes.Length];

                // Start decrypting.
                int decryptedByteCount = cryptoStream.Read(plainTextBytes,
                                                           0,
                                                           plainTextBytes.Length);

                // Close both streams.
                memoryStream.Close();
                cryptoStream.Close();

                // Convert decrypted data into a string. 
                // Let us assume that the original plaintext string was UTF8-encoded.
                string plainText = Encoding.UTF8.GetString(plainTextBytes,
                                                           0,
                                                           decryptedByteCount);

                // Return decrypted string.   
                return plainText;
            }
            catch (Exception exc)
            {
                var error = "Error Occured in Encrypting string:" + exc.Message;
                LoggingMethods.Logerr(error);
                return error;
            }
        }

        ///For references
        /// /http://blogs.msdn.com/b/buckh/archive/2006/03/15/552288.aspx
        /// ////http://social.msdn.microsoft.com/Forums/en/tfsworkitemtracking/thread/22a3a388-35c0-4761-a7c1-176a5b3e3f70
    }
}
